/*
 * Copyright (c) 2012-2017 Red Hat, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.che.plugin.docker.machine;

import static java.lang.String.format;
import static org.eclipse.che.plugin.docker.machine.DockerInstanceProvider.MACHINE_SNAPSHOT_PREFIX;

import com.google.common.annotations.VisibleForTesting;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.machine.Command;
import org.eclipse.che.api.core.model.machine.Machine;
import org.eclipse.che.api.core.model.machine.MachineSource;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.core.util.ListLineConsumer;
import org.eclipse.che.api.machine.server.exception.MachineException;
import org.eclipse.che.api.machine.server.model.impl.MachineRuntimeInfoImpl;
import org.eclipse.che.api.machine.server.spi.Instance;
import org.eclipse.che.api.machine.server.spi.InstanceProcess;
import org.eclipse.che.api.machine.server.spi.impl.AbstractInstance;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.lang.NameGenerator;
import org.eclipse.che.plugin.docker.client.DockerConnector;
import org.eclipse.che.plugin.docker.client.DockerConnectorProvider;
import org.eclipse.che.plugin.docker.client.Exec;
import org.eclipse.che.plugin.docker.client.LogMessage;
import org.eclipse.che.plugin.docker.client.ProgressLineFormatterImpl;
import org.eclipse.che.plugin.docker.client.params.CommitParams;
import org.eclipse.che.plugin.docker.client.params.CreateExecParams;
import org.eclipse.che.plugin.docker.client.params.GetResourceParams;
import org.eclipse.che.plugin.docker.client.params.PushParams;
import org.eclipse.che.plugin.docker.client.params.PutResourceParams;
import org.eclipse.che.plugin.docker.client.params.RemoveContainerParams;
import org.eclipse.che.plugin.docker.client.params.RemoveImageParams;
import org.eclipse.che.plugin.docker.client.params.StartExecParams;
import org.eclipse.che.plugin.docker.machine.node.DockerNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Docker implementation of {@link Instance}
 *
 * @author andrew00x
 * @author Alexander Garagatyi
 * @author Anton Korneta
 * @author Mykola Morhun
 */
public class DockerInstance extends AbstractInstance {
  private static final Logger LOG = LoggerFactory.getLogger(DockerInstance.class);

  /** Name of the latest tag used in Docker image. */
  public static final String LATEST_TAG = "latest";

  private static final AtomicInteger pidSequence = new AtomicInteger(1);
  private static final String PID_FILE_TEMPLATE = "/tmp/docker-exec-%s.pid";
  private static final Pattern PID_FILE_PATH_PATTERN =
      Pattern.compile(String.format(PID_FILE_TEMPLATE, "([0-9]+)"));
  /**
   * Produces output in form:
   *
   * <pre>
   * /some/path/pid_file_template-1.pid
   * /some/path/pid_file_template-3.pid
   * /some/path/pid_file_template-14.pid
   * </pre>
   *
   * Where each line is full path to pid file of <b>process that is running<b/>
   */
  private static final String GET_ALIVE_PROCESSES_COMMAND =
      format(
          "for pidFile in $(find %s -print 2>/dev/null); do kill -0 \"$(cat ${pidFile})\" 2>/dev/null && echo \"${pidFile}\"; done",
          format(PID_FILE_TEMPLATE, "*"));

  private final DockerMachineFactory dockerMachineFactory;
  private final String container;
  private final DockerConnector docker;
  private final String image;
  private final LineConsumer outputConsumer;
  private final String registry;
  private final String registryNamespace;
  private final DockerNode node;
  private final DockerInstanceStopDetector dockerInstanceStopDetector;
  private final DockerInstanceProcessesCleaner processesCleaner;
  private final ConcurrentHashMap<Integer, InstanceProcess> machineProcesses;
  private final boolean snapshotUseRegistry;
  private final MachineRuntimeInfoImpl machineRuntime;

  @Inject
  public DockerInstance(
      DockerConnectorProvider dockerProvider,
      @Named("che.docker.registry") String registry,
      @Named("che.docker.namespace") @Nullable String registryNamespace,
      DockerMachineFactory dockerMachineFactory,
      @Assisted Machine machine,
      @Assisted("container") String container,
      @Assisted("image") String image,
      @Assisted DockerNode node,
      @Assisted LineConsumer outputConsumer,
      DockerInstanceStopDetector dockerInstanceStopDetector,
      DockerInstanceProcessesCleaner processesCleaner,
      @Named("che.docker.registry_for_snapshots") boolean snapshotUseRegistry)
      throws MachineException {
    super(machine);
    this.dockerMachineFactory = dockerMachineFactory;
    this.container = container;
    this.docker = dockerProvider.get();
    this.image = image;
    this.outputConsumer = outputConsumer;
    this.registry = registry;
    this.registryNamespace = registryNamespace;
    this.node = node;
    this.dockerInstanceStopDetector = dockerInstanceStopDetector;
    this.processesCleaner = processesCleaner;
    this.machineProcesses = new ConcurrentHashMap<>();
    processesCleaner.trackProcesses(this);
    this.snapshotUseRegistry = snapshotUseRegistry;
    this.machineRuntime = doGetRuntime();
  }

  @Override
  public LineConsumer getLogger() {
    return outputConsumer;
  }

  @Override
  public MachineRuntimeInfoImpl getRuntime() {
    return machineRuntime;
  }

  @Override
  public InstanceProcess getProcess(final int pid) throws NotFoundException, MachineException {
    final InstanceProcess machineProcess = machineProcesses.get(pid);
    if (machineProcess != null) {
      try {
        machineProcess.checkAlive();
        return machineProcess;
      } catch (NotFoundException e) {
        machineProcesses.remove(pid);
        throw e;
      }
    }
    throw new NotFoundException(format("Process with pid %s not found", pid));
  }

  @Override
  public List<InstanceProcess> getProcesses() throws MachineException {
    List<InstanceProcess> processes = new LinkedList<>();
    try {
      final Exec exec =
          docker.createExec(
              CreateExecParams.create(
                      container, new String[] {"/bin/sh", "-c", GET_ALIVE_PROCESSES_COMMAND})
                  .withDetach(false));
      docker.startExec(
          StartExecParams.create(exec.getId()),
          logMessage -> {
            final String pidFilePath = logMessage.getContent().trim();
            final Matcher matcher = PID_FILE_PATH_PATTERN.matcher(pidFilePath);
            if (matcher.matches()) {
              final int virtualPid = Integer.parseInt(matcher.group(1));
              final InstanceProcess dockerProcess = machineProcesses.get(virtualPid);
              if (dockerProcess != null) {
                processes.add(dockerProcess);
              } else {
                LOG.warn(
                    "Machine process {} exists in container but missing in processes map",
                    virtualPid);
              }
            }
          });
      return processes;
    } catch (IOException e) {
      throw new MachineException(e);
    }
  }

  @Override
  public InstanceProcess createProcess(Command command, String outputChannel)
      throws MachineException {
    final Integer pid = pidSequence.getAndIncrement();
    final InstanceProcess process =
        dockerMachineFactory.createProcess(
            command, container, outputChannel, String.format(PID_FILE_TEMPLATE, pid), pid);
    machineProcesses.put(pid, process);
    return process;
  }

  @Override
  public MachineSource saveToSnapshot() throws MachineException {
    try {
      String image = generateRepository();
      if (!snapshotUseRegistry) {
        commitContainer(image, LATEST_TAG);
        return new DockerMachineSource(image).withTag(LATEST_TAG);
      }

      PushParams pushParams = PushParams.create(image).withRegistry(registry).withTag(LATEST_TAG);

      final String fullRepo = pushParams.getFullRepo();
      commitContainer(fullRepo, LATEST_TAG);
      // TODO fix this workaround. Docker image is not visible after commit when using swarm
      Thread.sleep(2000);
      final ProgressLineFormatterImpl lineFormatter = new ProgressLineFormatterImpl();
      final String digest =
          docker.push(
              pushParams,
              progressMonitor -> {
                try {
                  outputConsumer.writeLine(lineFormatter.format(progressMonitor));
                } catch (IOException ignored) {
                }
              });
      docker.removeImage(RemoveImageParams.create(fullRepo).withForce(false));
      return new DockerMachineSource(image)
          .withRegistry(registry)
          .withDigest(digest)
          .withTag(LATEST_TAG);
    } catch (IOException ioEx) {
      throw new MachineException(ioEx);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new MachineException(e.getLocalizedMessage(), e);
    }
  }

  @VisibleForTesting
  protected void commitContainer(String repository, String tag) throws IOException {
    String comment =
        format("Suspended at %1$ta %1$tb %1$td %1$tT %1$tZ %1$tY", System.currentTimeMillis());
    // !! We SHOULD NOT pause container before commit because all execs will fail
    // to push image to private registry it should be tagged with registry in repo name
    // https://docs.docker.com/reference/api/docker_remote_api_v1.16/#push-an-image-on-the-registry
    docker.commit(
        CommitParams.create(container)
            .withRepository(repository)
            .withTag(tag)
            .withComment(comment));
  }

  private String generateRepository() {
    if (registryNamespace != null) {
      return registryNamespace + '/' + MACHINE_SNAPSHOT_PREFIX + NameGenerator.generate(null, 16);
    }
    return MACHINE_SNAPSHOT_PREFIX + NameGenerator.generate(null, 16);
  }

  @Override
  public void destroy() throws MachineException {
    try {
      outputConsumer.close();
    } catch (IOException ignored) {
    }

    machineProcesses.clear();
    processesCleaner.untrackProcesses(getId());
    dockerInstanceStopDetector.stopDetection(container);
    try {
      if (getConfig().isDev()) {
        node.unbindWorkspace();
      }

      // kill container is not needed here, because we removing container with force flag
      docker.removeContainer(
          RemoveContainerParams.create(container).withRemoveVolumes(true).withForce(true));
    } catch (IOException | ServerException e) {
      LOG.error(e.getLocalizedMessage(), e);
      throw new MachineException(e.getLocalizedMessage());
    }

    try {
      docker.removeImage(RemoveImageParams.create(image).withForce(false));
    } catch (IOException ignore) {
      LOG.error("IOException during destroy(). Ignoring.");
    }
  }

  @Override
  public DockerNode getNode() {
    return node;
  }

  /**
   * Reads file content by specified file path.
   *
   * <p>TODO: add file size checking, note that size checking and file content reading should be
   * done in an atomic way, which means that two separate instance processes is not the case.
   *
   * @param filePath path to file on machine instance
   * @param startFrom line number to start reading from
   * @param limit limitation on line
   * @return if {@code limit} and {@code startFrom} grater than 0 content from {@code startFrom} to
   *     {@code startFrom + limit} will be returned, if file contains less lines than {@code
   *     startFrom} empty content will be returned
   * @throws MachineException if any error occurs with file reading
   */
  @Override
  public String readFileContent(String filePath, int startFrom, int limit) throws MachineException {
    if (limit <= 0 || startFrom <= 0) {
      throw new MachineException(
          "Impossible to read file " + limit + " lines from " + startFrom + " line");
    }

    // command sed getting file content from startFrom line to (startFrom + limit)
    String shCommand =
        format("sed -n \'%1$2s, %2$2sp\' %3$2s", startFrom, startFrom + limit, filePath);

    final String[] command = {"/bin/sh", "-c", shCommand};

    ListLineConsumer lines = new ListLineConsumer();
    try {
      Exec exec = docker.createExec(CreateExecParams.create(container, command).withDetach(false));
      docker.startExec(
          StartExecParams.create(exec.getId()),
          new LogMessagePrinter(lines, LogMessage::getContent));
    } catch (IOException e) {
      throw new MachineException(
          format(
              "Error occurs while initializing command %s in docker container %s: %s",
              Arrays.toString(command), container, e.getLocalizedMessage()),
          e);
    }

    String content = lines.getText();
    if (content.contains("sed: can't read " + filePath + ": No such file or directory")
        || content.contains("cat: " + filePath + ": No such file or directory")) {
      throw new MachineException("File with path " + filePath + " not found");
    }
    return content;
  }

  @Override
  public void copy(
      Instance sourceMachine, String sourcePath, String targetPath, boolean overwriteDirNonDir)
      throws MachineException {
    if (!(sourceMachine instanceof DockerInstance)) {
      throw new MachineException("Unsupported copying between not docker machines");
    }
    try {
      docker.putResource(
          PutResourceParams.create(
                  container,
                  targetPath,
                  docker.getResource(
                      GetResourceParams.create(
                          ((DockerInstance) sourceMachine).container, sourcePath)))
              .withNoOverwriteDirNonDir(overwriteDirNonDir));
    } catch (IOException e) {
      throw new MachineException(e.getLocalizedMessage());
    }
  }

  /**
   * Not implemented.
   *
   * <p>{@inheritDoc}
   */
  @Override
  public void copy(String sourcePath, String targetPath) throws MachineException {
    throw new MachineException("Unsupported operation for docker machine implementation");
  }

  /**
   * Removes process from the list of processes
   *
   * <p>Used by {@link DockerInstanceProcessesCleaner}
   */
  void removeProcess(int pid) {
    machineProcesses.remove(pid);
  }

  /** Can be used for docker specific operations with machine */
  public String getContainer() {
    return container;
  }

  private MachineRuntimeInfoImpl doGetRuntime() throws MachineException {
    try {
      return new MachineRuntimeInfoImpl(
          dockerMachineFactory.createMetadata(
              docker.inspectContainer(container), getConfig(), node.getHost()));
    } catch (IOException x) {
      throw new MachineException(x.getMessage(), x);
    }
  }
}
